// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © M1D
//@version=6
indicator("Macro Ranges+ (M1D)", "Macro Ranges+", overlay = true, max_boxes_count = 300, max_lines_count = 300, max_labels_count = 300)

// Each ICT macro window boxed around the range price actually made inside it: the box runs from the
// window's first candle to its last, high to low, and grows live while the window is open. No full-height
// strip. The macro's open time sits above the box; hover it for the range.

const string TZ     = "America/New_York"
const int    DAY_MS = 86400000
const color  INK    = #000000
const color  C_UP   = #1d4ed8
const color  C_DN   = #f23645

// ─── Inputs ──────────────────────────────────────────────────────────────────
const string G_W = "Macros"
const string G_D = "Drawing"
const string G_C = "Next macro chip"

string winMode  = input.string("ICT list", "Windows", options = ["ICT list", "Every hour (:50 to :10)"], group = G_W, tooltip = "ICT list, New York time: 02:33-03:00, 04:03-04:30, 08:50-09:10, 09:50-10:10, 10:50-11:10, 11:50-12:10, 13:10-13:40, 15:15-15:45, plus the Asia macros 18:50-19:10 through 23:50-00:10. Every hour: XX:50 to XX+1:10 round the clock.")
int    maxTf    = input.int(5, "Highest chart timeframe (minutes)", minval = 1, maxval = 15, group = G_W, tooltip = "A macro is 20 to 30 minutes, so it only has a range worth boxing on low timeframes.")
int    daysBack = input.int(3, "History (days)", minval = 1, maxval = 30, group = G_W)
string colMode  = input.string("Direction", "Colour", options = ["Direction", "Grey"], group = G_D, tooltip = "Direction: blue when the macro closed above its open, red when below. Grey: one neutral colour.")
int    fillTr   = input.int(85, "Fill transparency", minval = 50, maxval = 98, group = G_D)
bool   showCe   = input.bool(true, "Midpoint (CE)", group = G_D, tooltip = "A dotted line across the box at the middle of the macro's range.")
bool   extHL    = input.bool(false, "Extend high / low until taken", group = G_D, tooltip = "After the macro closes, its high and low run right as dotted lines until price trades through them.")
bool   showName = input.bool(true, "Time above the box", group = G_D)
string txtIn    = input.string("small", "Text size", options = ["tiny", "small", "normal"], group = G_D)
bool   showChip = input.bool(true, "Show the next macro", group = G_C, tooltip = "A small card: the macro running now and the minutes it has left, or the next one and how long until it opens.")
string chipPos  = input.string("Bottom Right", "Position", options = ["Top Right", "Bottom Right", "Top Left", "Bottom Left"], group = G_C)
string edgeIn   = input.string("None", "Box edge", options = ["None", "Dotted", "Solid"], group = G_D, tooltip = "None: the fill alone shows the range. Dotted: a light dotted outline. Solid: a hard outline.")

f_edge(color c) =>
    edgeIn == "None" ? color.new(c, fillTr) : color.new(c, 0)
string EDGE_STY = edgeIn == "Dotted" ? line.style_dotted : line.style_solid

string SZ = txtIn == "tiny" ? size.tiny : txtIn == "normal" ? size.normal : size.small

// ─── Windows (minutes from midnight, New York) ───────────────────────────────
var array<int> wS = array.new<int>()
var array<int> wE = array.new<int>()
if barstate.isfirst
    if winMode == "ICT list"
        wS.push(153), wE.push(180)
        wS.push(243), wE.push(270)
        wS.push(530), wE.push(550)
        wS.push(590), wE.push(610)
        wS.push(650), wE.push(670)
        wS.push(710), wE.push(730)
        wS.push(790), wE.push(820)
        wS.push(915), wE.push(945)
        // Asia (18:00 to midnight New York): the hourly :50 to :10 macros. The last one runs past midnight.
        for h = 18 to 23
            wS.push(h * 60 + 50)
            wE.push(h * 60 + 70)
    else
        for h = 0 to 23
            wS.push(h * 60 + 50)
            wE.push(h * 60 + 70)

int  tfMin = timeframe.in_seconds() / 60
bool tfOk  = timeframe.in_seconds() < 60 * maxTf + 1 and timeframe.isintraday
int  mNow  = hour(time, TZ) * 60 + minute(time, TZ)

//@function The window this bar overlaps, or -1. A window running past midnight wraps.
f_win(int m) =>
    int out = -1
    for i = 0 to wS.size() - 1
        int s = wS.get(i)
        int e = wE.get(i)
        // the bar spans [m, m + tfMin); it belongs to the window it overlaps
        bool hit = (m < e and m + tfMin > s) or (e > 1440 and m + 1440 < e and m + 1440 + tfMin > s)
        if hit and out < 0
            out := i
    out

f_hhmm(int m) =>
    int mm = m % 1440
    str.format("{0}:{1}", str.tostring(int(mm / 60), "00"), str.tostring(mm % 60, "00"))

// ─── State ───────────────────────────────────────────────────────────────────
type Macro
    int   t0
    float o
    float hi
    float lo
    box   bx
    line  ce
    label nm
    line  hiL
    line  loL
    bool  live = true
    bool  hiTaken = false
    bool  loTaken = false
    string name

var array<Macro> macros = array.new<Macro>()
var int curWin = -1

int  w      = tfOk ? f_win(mNow) : -1
bool newWin = w >= 0 and (w != curWin or (not na(time[1]) and time - time[1] > 30 * 60000))

f_colour(Macro m) =>
    colMode == "Grey" ? #787B86 : close >= m.o ? C_UP : C_DN

// Close the running macro when the bar leaves it.
if macros.size() > 0
    Macro last = macros.last()
    if last.live and (w < 0 or newWin)
        last.live := false
        if extHL
            last.hiL := line.new(time[1], last.hi, time, last.hi, xloc = xloc.bar_time, color = INK, width = 1, style = line.style_dotted)
            last.loL := line.new(time[1], last.lo, time, last.lo, xloc = xloc.bar_time, color = INK, width = 1, style = line.style_dotted)

if newWin
    string nmTxt = f_hhmm(wS.get(w))
    Macro m = Macro.new(t0 = time, o = open, hi = high, lo = low, name = nmTxt)
    color c = colMode == "Grey" ? #787B86 : close >= open ? C_UP : C_DN
    m.bx := box.new(time, high, time, low, xloc = xloc.bar_time, bgcolor = color.new(c, fillTr), border_color = f_edge(c), border_width = 1, border_style = EDGE_STY)
    if showCe
        m.ce := line.new(time, math.avg(high, low), time, math.avg(high, low), xloc = xloc.bar_time, color = color.new(c, 0), width = 1, style = line.style_dotted)
    if showName
        m.nm := label.new(time, high, nmTxt, xloc = xloc.bar_time, style = label.style_label_down, color = color.new(#FFFFFF, 100), textcolor = INK, size = SZ)
        label.set_text_font_family(m.nm, font.family_monospace)
    macros.push(m)
    curWin := w
else if w < 0
    curWin := -1

// Grow the running macro.
if macros.size() > 0 and w >= 0 and not newWin
    Macro m = macros.last()
    if m.live
        m.hi := math.max(m.hi, high)
        m.lo := math.min(m.lo, low)
        color c = f_colour(m)
        box.set_top(m.bx, m.hi)
        box.set_bottom(m.bx, m.lo)
        box.set_right(m.bx, time)
        box.set_bgcolor(m.bx, color.new(c, fillTr))
        box.set_border_color(m.bx, f_edge(c))
        if not na(m.ce)
            line.set_xy1(m.ce, m.t0, math.avg(m.hi, m.lo))
            line.set_xy2(m.ce, time, math.avg(m.hi, m.lo))
            line.set_color(m.ce, color.new(c, 0))
        if not na(m.nm)
            label.set_xy(m.nm, int(math.avg(m.t0, time)), m.hi)

// Tooltip, extended levels, history.
if macros.size() > 0
    for i = macros.size() - 1 to 0
        Macro m = macros.get(i)
        if time - m.t0 > daysBack * DAY_MS
            box.delete(m.bx), line.delete(m.ce), label.delete(m.nm), line.delete(m.hiL), line.delete(m.loL)
            macros.remove(i)
            continue
        if not m.live and extHL
            if not m.hiTaken and not na(m.hiL)
                line.set_x2(m.hiL, time)
                if high > m.hi
                    m.hiTaken := true
            if not m.loTaken and not na(m.loL)
                line.set_x2(m.loL, time)
                if low < m.lo
                    m.loTaken := true
// Every macro can age out at once (a stock on Monday before its first macro): read the last only if one is left.
if macros.size() > 0
    Macro m = macros.last()
    if not na(m.nm)
        label.set_tooltip(m.nm, m.name + " macro: high " + str.tostring(m.hi, format.mintick) + ", low " + str.tostring(m.lo, format.mintick) + ", range " + str.tostring(m.hi - m.lo, format.mintick) + " pts.")

// ─── Next macro chip ─────────────────────────────────────────────────────────
var table chip = na
if barstate.islast and showChip
    if na(chip)
        chip := table.new(chipPos == "Top Right" ? position.top_right : chipPos == "Top Left" ? position.top_left : chipPos == "Bottom Left" ? position.bottom_left : position.bottom_right, 1, 1, bgcolor = #131722, frame_color = #131722, frame_width = 4)
    // Use the clock, not the bar, so the countdown is right between bars.
    int mClk = hour(timenow, TZ) * 60 + minute(timenow, TZ)
    string txt = ""
    int best = 100000
    for i = 0 to wS.size() - 1
        int s = wS.get(i)
        int e = wE.get(i)
        // A window past midnight (23:50 to 00:10) is also caught on the far side of midnight.
        int mIn = mClk >= s and mClk < e ? mClk : mClk + 1440 >= s and mClk + 1440 < e ? mClk + 1440 : -1
        if mIn >= 0
            txt := "In macro " + f_hhmm(s) + " · " + str.tostring(e - mIn) + "m left"
            best := -1
        else if best >= 0
            int until = (s - mClk + 1440) % 1440
            if until < best
                best := until
                txt := "Next macro " + f_hhmm(s) + " · in " + (until >= 60 ? str.tostring(int(until / 60)) + "h " : "") + str.tostring(until % 60) + "m"
    // No bar for over 30 minutes: the market is shut (halt, weekend), so a countdown would mislead.
    if timenow - time_close > 30 * 60000
        txt := "Market closed"
    // Above the timeframe limit nothing draws: say why rather than leave a blank chart.
    if not tfOk
        txt := "Macros draw on " + str.tostring(maxTf) + "m charts or lower"
    table.cell(chip, 0, 0, txt, text_color = #D1D4DC, text_size = SZ, text_font_family = font.family_monospace)
